
/*
 * Copyright (c) 2010 Sergey N Frolov
 * 
 * See LICENSE file
 */

#include "ngx_http_javascript_module_file.h"

static JSFunctionSpec ngx_javascript_file_class_methods[] = {
	{ "open", ngx_javascript_file_class_open, 0, 0, 0 },
	{ "stat", ngx_javascript_file_class_stat, 0, 0, 0 },
	{ "lstat", ngx_javascript_file_class_lstat, 0, 0, 0 },
	{ "basename", ngx_javascript_file_class_basename, 0, 0, 0 },
	{ "dirname", ngx_javascript_file_class_dirname, 0, 0, 0 },
	{ "realpath", ngx_javascript_file_class_realpath, 0, 0, 0 },
	{ "chmod", ngx_javascript_file_class_chmod, 0, 0, 0 },
	{ "copy", ngx_javascript_file_class_copy, 0, 0, 0 },
	{ "exists", ngx_javascript_file_class_exists, 0, 0, 0 },
	{ "isDir", ngx_javascript_file_class_is_dir, 0, 0, 0 },
	{ "isFile", ngx_javascript_file_class_is_file, 0, 0, 0 },
	{ "isLink", ngx_javascript_file_class_is_link, 0, 0, 0 },
	{ "isReadable", ngx_javascript_file_class_is_readable, 0, 0, 0 },
	{ "isWritable", ngx_javascript_file_class_is_writable, 0, 0, 0 },
	{ "isExecutable", ngx_javascript_file_class_is_executable, 0, 0, 0 },
	{ "link", ngx_javascript_file_class_link, 0, 0, 0 },
	{ "mkdir", ngx_javascript_file_class_mkdir, 0, 0, 0 },
	{ "readlink", ngx_javascript_file_class_readlink, 0, 0, 0 },
	{ "rmdir", ngx_javascript_file_class_rmdir, 0, 0, 0 },
	{ "symlink", ngx_javascript_file_class_symlink, 0, 0, 0 },
	{ "unlink", ngx_javascript_file_class_unlink, 0, 0, 0 },
	{ 0, 0, 0, 0, 0 }
};

static JSFunctionSpec ngx_javascript_file_object_methods[] = {
	{ "read", ngx_javascript_file_object_get_chars, 0, 0, 0 },
	{ "getBytes", ngx_javascript_file_object_get_bytes, 0, 0, 0 },
	{ "getChars", ngx_javascript_file_object_get_chars, 0, 0, 0 },
	{ "write", ngx_javascript_file_object_write_chars, 0, 0, 0 },
	{ "writeBytes", ngx_javascript_file_object_write_bytes, 0, 0, 0 },
	{ "writeChars", ngx_javascript_file_object_write_chars, 0, 0, 0 },
	{ "stat", ngx_javascript_file_object_stat, 0, 0, 0 },
	{ "close", ngx_javascript_file_object_close, 0, 0, 0 },
	{ "eof", ngx_javascript_file_object_eof, 0, 0, 0 },
	{ "lock", ngx_javascript_file_object_lock, 0, 0, 0 },
	{ "seek", ngx_javascript_file_object_seek, 0, 0, 0 },
	{ "tell", ngx_javascript_file_object_tell, 0, 0, 0 },
	{ "realpath", ngx_javascript_file_object_realpath, 0, 0, 0 },
	{ "basename", ngx_javascript_file_object_basename, 0, 0, 0 },
	{ "dirname", ngx_javascript_file_object_dirname, 0, 0, 0 },
	{ "flush", ngx_javascript_file_object_flush, 0, 0, 0 },
	{ "truncate", ngx_javascript_file_object_truncate, 0, 0, 0 },
	{ "rewind", ngx_javascript_file_object_rewind, 0, 0, 0 },
	{ 0, 0, 0, 0, 0 }
};

static JSClass ngx_javascript_file_class = {
	"File",
	JSCLASS_HAS_PRIVATE,
	JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub,
	JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, ngx_javascript_file_finalize,
	JSCLASS_NO_OPTIONAL_MEMBERS
};

JSBool ngx_javascript_init_file(JSContext *jcx, JSObject *global) {
	jsval result;
	if (JS_InitClass(jcx, global, NULL, &ngx_javascript_file_class, ngx_javascript_file_object, 0, NULL, ngx_javascript_file_object_methods, NULL, ngx_javascript_file_class_methods) == NULL)
		return ngx_javascript_outofmemory(jcx);
	if (JS_LookupProperty(jcx, global, "File", &result) == JS_TRUE && JSVAL_IS_OBJECT(result) && !JSVAL_IS_NULL(result)) {
		JSObject *file_class = JSVAL_TO_OBJECT(result);
		JS_DefineProperty(jcx, file_class, "SEEK_SET", INT_TO_JSVAL(SEEK_SET), NULL, NULL, 0);
		JS_DefineProperty(jcx, file_class, "SEEK_CUR", INT_TO_JSVAL(SEEK_CUR), NULL, NULL, 0);
		JS_DefineProperty(jcx, file_class, "SEEK_END", INT_TO_JSVAL(SEEK_END), NULL, NULL, 0);
		JS_DefineProperty(jcx, file_class, "LOCK_SH", INT_TO_JSVAL(LOCK_SH), NULL, NULL, 0);
		JS_DefineProperty(jcx, file_class, "LOCK_EX", INT_TO_JSVAL(LOCK_EX), NULL, NULL, 0);
		JS_DefineProperty(jcx, file_class, "LOCK_UN", INT_TO_JSVAL(LOCK_UN), NULL, NULL, 0);
		JS_DefineProperty(jcx, file_class, "LOCK_NB", INT_TO_JSVAL(LOCK_NB), NULL, NULL, 0);
	}
	return JS_TRUE;
}

JSBool ngx_javascript_file_object(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_private_t *private;
	ngx_javascript_file_t *file;
	char *filename, *modename = "r";
	int mode = 0;
	FILE *F;
	
	if (JS_ConvertArguments(jcx, argc, argv, "s/s", &filename, &modename) == JS_FALSE) return JS_FALSE;
	
	if (strchr(modename, 'w') != NULL || strchr(modename, 'a') != NULL) mode = O_CREAT;
	if (filename[0] == '/') {
		if (ngx_javascript_check_basedir(jcx, filename, mode) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, filename);
		if ((F = fopen(filename, modename)) == NULL) return ngx_javascript_file_error(jcx, ngx_errno, filename);
	} else {
		char fullname[NGX_MAX_PATH];
		if (ngx_javascript_fullpath(jcx, filename, fullname) == JS_FALSE) return JS_FALSE;
		if (ngx_javascript_check_basedir(jcx, fullname, mode) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, fullname);
		if ((F = fopen(fullname, modename)) == NULL) return ngx_javascript_file_error(jcx, ngx_errno, fullname);
	}
	
	private = JS_GetContextPrivate(jcx);
	if (private == NULL || private->r == NULL) return ngx_javascript_error(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	
	file = ngx_pcalloc(private->r->pool, sizeof(ngx_javascript_file_t));
	if (file == NULL) {
		fclose(F);
		return ngx_javascript_outofmemory(jcx);
	}
	file->F = F;
	file->fd = fileno(F);
	JS_SetPrivate(jcx, obj, file);
	JS_DefineProperty(jcx, obj, "name", argv[0], NULL, NULL, JSPROP_ENUMERATE | JSPROP_READONLY);
	
	return JS_TRUE;
}

jsval ngx_javascript_stat_value(JSContext *jcx, struct stat *st) {
	jsval result;
	JSObject *t;
	JSObject *r = JS_NewObject(jcx, NULL, NULL, NULL);
	if (r == NULL) {
		ngx_javascript_outofmemory(jcx);
		return JSVAL_VOID;
	}
	if (JS_NewNumberValue(jcx, (jsdouble) st->st_size, &result) == JS_TRUE) JS_DefineProperty(jcx, r, "size", result, NULL, NULL, JSPROP_ENUMERATE);
	JS_DefineProperty(jcx, r, "mode", INT_TO_JSVAL((int) st->st_mode), NULL, NULL, JSPROP_ENUMERATE);
	JS_DefineProperty(jcx, r, "uid", INT_TO_JSVAL((int) st->st_uid), NULL, NULL, JSPROP_ENUMERATE);
	JS_DefineProperty(jcx, r, "gid", INT_TO_JSVAL((int) st->st_gid), NULL, NULL, JSPROP_ENUMERATE);
	t = js_NewDateObjectMsec(jcx, ((jsdouble) st->st_mtime) * 1000);
	if (t != NULL) JS_DefineProperty(jcx, r, "mtime", OBJECT_TO_JSVAL(t), NULL, NULL, JSPROP_ENUMERATE);
	t = js_NewDateObjectMsec(jcx, ((jsdouble) st->st_atime) * 1000);
	if (t != NULL) JS_DefineProperty(jcx, r, "atime", OBJECT_TO_JSVAL(t), NULL, NULL, JSPROP_ENUMERATE);
	t = js_NewDateObjectMsec(jcx, ((jsdouble) st->st_ctime) * 1000);
	if (t != NULL) JS_DefineProperty(jcx, r, "ctime", OBJECT_TO_JSVAL(t), NULL, NULL, JSPROP_ENUMERATE);
	return OBJECT_TO_JSVAL(r);
}

JSBool ngx_javascript_file_stat_name(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval, struct stat *st, int link, int ex) {
	char *filename;
	if (JS_ConvertArguments(jcx, argc, argv, "s", &filename) == JS_FALSE) return JS_FALSE;
	if (filename[0] == '/') {
		if (ngx_javascript_check_basedir(jcx, filename, 0) == JS_FALSE) return ex ? ngx_javascript_file_error(jcx, ENOENT, filename) : JS_FALSE;
		if (link) {
			if (lstat(filename, st) == -1) return ex ? ngx_javascript_file_error(jcx, ngx_errno, filename) : JS_FALSE;
		} else {
			if (stat(filename, st) == -1) return ex ? ngx_javascript_file_error(jcx, ngx_errno, filename) : JS_FALSE;
		}
	} else {
		char fullname[NGX_MAX_PATH];
		if (ngx_javascript_fullpath(jcx, filename, fullname) == JS_FALSE) return JS_FALSE;
		if (ngx_javascript_check_basedir(jcx, fullname, 0) == JS_FALSE) return ex ? ngx_javascript_file_error(jcx, ENOENT, fullname) : JS_FALSE;
		if (link) {
			if (lstat(fullname, st) == -1) return ex ? ngx_javascript_file_error(jcx, ngx_errno, fullname) : JS_FALSE;
		} else {
			if (stat(fullname, st) == -1) return ex ? ngx_javascript_file_error(jcx, ngx_errno, fullname) : JS_FALSE;
		}
	}
	return JS_TRUE;
}

JSBool ngx_javascript_file_access(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval, int mode, int ex) {
	char *filename;
	if (JS_ConvertArguments(jcx, argc, argv, "s", &filename) == JS_FALSE) return JS_FALSE;
	if (filename[0] == '/') {
		if (ngx_javascript_check_basedir(jcx, filename, 0) == JS_FALSE) return ex ? ngx_javascript_file_error(jcx, ENOENT, filename) : JS_FALSE;
		if (access(filename, mode) != 0) return ex ? ngx_javascript_file_error(jcx, ngx_errno, filename) : JS_FALSE;
	} else {
		char fullname[NGX_MAX_PATH];
		if (ngx_javascript_fullpath(jcx, filename, fullname) == JS_FALSE) return JS_FALSE;
		if (ngx_javascript_check_basedir(jcx, fullname, 0) == JS_FALSE) return ex ? ngx_javascript_file_error(jcx, ENOENT, fullname) : JS_FALSE;
		if (access(fullname, mode) != 0) return ex ? ngx_javascript_file_error(jcx, ngx_errno, fullname) : JS_FALSE;
	}
	return JS_TRUE;
}

JSBool ngx_javascript_file_stat_file(JSContext *jcx, JSObject *obj, struct stat *st) {
	ngx_javascript_file_t *file;
	file = JS_GetPrivate(jcx, obj);
	if (file == NULL || file->F == NULL) return ngx_javascript_file_error(jcx, 0, "File is closed");
	if (fstat(file->fd, st) != 0) return ngx_javascript_file_error(jcx, ngx_errno, NULL);
	return JS_TRUE;
}



JSBool ngx_javascript_file_class_open(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	JSObject *r = JS_NewObject(jcx, &ngx_javascript_file_class, NULL, NULL);
	if (r == NULL) return ngx_javascript_outofmemory(jcx);
	*rval = OBJECT_TO_JSVAL(r);
	if (ngx_javascript_file_object(jcx, r, argc, argv, rval) == JS_FALSE) return JS_FALSE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_stat(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	struct stat st;
	if (ngx_javascript_file_stat_name(jcx, obj, argc, argv, rval, &st, 0, 1) == JS_FALSE) return JS_FALSE;
	*rval = ngx_javascript_stat_value(jcx, &st);
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_is_dir(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	struct stat st;
	if (ngx_javascript_file_stat_name(jcx, obj, argc, argv, rval, &st, 0, 1) == JS_FALSE) return JS_FALSE;
	*rval = S_ISDIR(st.st_mode) ? JSVAL_TRUE : JSVAL_FALSE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_is_file(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	struct stat st;
	if (ngx_javascript_file_stat_name(jcx, obj, argc, argv, rval, &st, 0, 1) == JS_FALSE) return JS_FALSE;
	*rval = S_ISREG(st.st_mode) ? JSVAL_TRUE : JSVAL_FALSE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_is_link(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	struct stat st;
	if (ngx_javascript_file_stat_name(jcx, obj, argc, argv, rval, &st, 0, 1) == JS_FALSE) return JS_FALSE;
	*rval = S_ISLNK(st.st_mode) ? JSVAL_TRUE : JSVAL_FALSE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_is_readable(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	*rval = ngx_javascript_file_access(jcx, obj, argc, argv, rval, R_OK, 0);
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_is_writable(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	*rval = ngx_javascript_file_access(jcx, obj, argc, argv, rval, W_OK, 0);
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_is_executable(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	*rval = ngx_javascript_file_access(jcx, obj, argc, argv, rval, X_OK, 0);
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_lstat(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	struct stat st;
	if (ngx_javascript_file_stat_name(jcx, obj, argc, argv, rval, &st, 1, 1) == JS_FALSE) return JS_FALSE;
	*rval = ngx_javascript_stat_value(jcx, &st);
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_realpath(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	char *filename;
	char realname[NGX_MAX_PATH];
	if (JS_ConvertArguments(jcx, argc, argv, "s", &filename) == JS_FALSE) return JS_FALSE;
	if (realpath(filename, realname) != NULL) {
		if (ngx_javascript_check_basedir_realpath(jcx, realname) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, filename);
		JSString *str = JS_NewStringCopyZ(jcx, realname);
		if (str == NULL) return ngx_javascript_outofmemory(jcx);
		*rval = STRING_TO_JSVAL(str);
	} else {
		*rval = JSVAL_NULL;
	}
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_basename(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	char *filename;
	int i, l, length;
	if (JS_ConvertArguments(jcx, argc, argv, "s", &filename) == JS_FALSE) return JS_FALSE;
	length = strlen(filename);
	i = length - 1;
	while (i >= 0 && filename[i] == '/') i --;
	l = i;
	while (i >= 0 && filename[i] != '/') i --;
	if (length <= 0 || i == l) {
		*rval = JS_GetEmptyStringValue(jcx);
		return JS_TRUE;
	} else {
		JSString *str = JS_NewStringCopyN(jcx, filename + i + 1, l - i);
		if (str == NULL) return ngx_javascript_outofmemory(jcx);
		*rval = STRING_TO_JSVAL(str);
	}
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_dirname(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	char *filename;
	int i, length;
	if (JS_ConvertArguments(jcx, argc, argv, "s", &filename) == JS_FALSE) return JS_FALSE;
	length = strlen(filename);
	i = length - 1;
	while (i >= 0 && filename[i] == '/') i --;
	while (i >= 0 && filename[i] != '/') i --;
	if (length <= 0 || i <= 0) {
		JSString *str = JS_NewStringCopyZ(jcx, ".");
		if (str == NULL) return ngx_javascript_outofmemory(jcx);
		*rval = STRING_TO_JSVAL(str);
	} else {
		JSString *str = JS_NewStringCopyN(jcx, filename, i + 1);
		if (str == NULL) return ngx_javascript_outofmemory(jcx);
		*rval = STRING_TO_JSVAL(str);
	}
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_chmod(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	char *filename;
	int mode;
	if (JS_ConvertArguments(jcx, argc, argv, "si", &filename, &mode) == JS_FALSE) return JS_FALSE;
	if (filename[0] == '/') {
		if (ngx_javascript_check_basedir(jcx, filename, 0) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, filename);
		if (chmod(filename, mode) != 0) return ngx_javascript_file_error(jcx, ngx_errno, filename);
	} else {
		char fullname[NGX_MAX_PATH];
		if (ngx_javascript_fullpath(jcx, filename, fullname) == JS_FALSE) return JS_FALSE;
		if (ngx_javascript_check_basedir(jcx, fullname, 0) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, fullname);
		if (chmod(fullname, mode) != 0) return ngx_javascript_file_error(jcx, ngx_errno, fullname);
	}
	*rval = JS_TRUE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_mkdir(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	char *filename;
	int mode = 777;
	if (JS_ConvertArguments(jcx, argc, argv, "s/i", &filename, &mode) == JS_FALSE) return JS_FALSE;
	if (filename[0] == '/') {
		if (ngx_javascript_check_basedir(jcx, filename, O_CREAT) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, filename);
		if (mkdir(filename, mode) != 0) return ngx_javascript_file_error(jcx, ngx_errno, filename);
	} else {
		char fullname[NGX_MAX_PATH];
		if (ngx_javascript_fullpath(jcx, filename, fullname) == JS_FALSE) return JS_FALSE;
		if (ngx_javascript_check_basedir(jcx, fullname, O_CREAT) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, fullname);
		if (mkdir(fullname, mode) != 0) return ngx_javascript_file_error(jcx, ngx_errno, fullname);
	}
	*rval = JS_TRUE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_readlink(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	char *filename;
	char filelink[NGX_MAX_PATH];
	JSString *str;
	if (JS_ConvertArguments(jcx, argc, argv, "s", &filename) == JS_FALSE) return JS_FALSE;
	memset(filelink, 0, NGX_MAX_PATH);
	if (filename[0] == '/') {
		if (ngx_javascript_check_basedir(jcx, filename, 0) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, filename);
		if (readlink(filename, filelink, NGX_MAX_PATH) != 0) return ngx_javascript_file_error(jcx, ngx_errno, filename);
	} else {
		char fullname[NGX_MAX_PATH];
		if (ngx_javascript_fullpath(jcx, filename, fullname) == JS_FALSE) return JS_FALSE;
		if (ngx_javascript_check_basedir(jcx, fullname, 0) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, fullname);
		if (readlink(fullname, filelink, NGX_MAX_PATH) != 0) return ngx_javascript_file_error(jcx, ngx_errno, fullname);
	}
	str = JS_NewStringCopyZ(jcx, filelink);
	if (str == NULL) return ngx_javascript_outofmemory(jcx);
	*rval = STRING_TO_JSVAL(str);
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_rmdir(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	char *filename;
	if (JS_ConvertArguments(jcx, argc, argv, "s", &filename) == JS_FALSE) return JS_FALSE;
	if (filename[0] == '/') {
		if (ngx_javascript_check_basedir(jcx, filename, 0) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, filename);
		if (rmdir(filename) != 0) return ngx_javascript_file_error(jcx, ngx_errno, filename);
	} else {
		char fullname[NGX_MAX_PATH];
		if (ngx_javascript_fullpath(jcx, filename, fullname) == JS_FALSE) return JS_FALSE;
		if (ngx_javascript_check_basedir(jcx, fullname, 0) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, fullname);
		if (rmdir(fullname) != 0) return ngx_javascript_file_error(jcx, ngx_errno, fullname);
	}
	*rval = JSVAL_TRUE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_unlink(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	char *filename;
	if (JS_ConvertArguments(jcx, argc, argv, "s", &filename) == JS_FALSE) return JS_FALSE;
	if (filename[0] == '/') {
		if (ngx_javascript_check_basedir(jcx, filename, 0) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, filename);
		if (unlink(filename) != 0) return ngx_javascript_file_error(jcx, ngx_errno, filename);
	} else {
		char fullname[NGX_MAX_PATH];
		if (ngx_javascript_fullpath(jcx, filename, fullname) == JS_FALSE) return JS_FALSE;
		if (ngx_javascript_check_basedir(jcx, fullname, 0) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, fullname);
		if (unlink(fullname) != 0) return ngx_javascript_file_error(jcx, ngx_errno, fullname);
	}
	*rval = JSVAL_TRUE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_exists(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	struct stat st;
	if (ngx_javascript_file_stat_name(jcx, obj, argc, argv, rval, &st, 0, 0) == JS_FALSE) *rval = JSVAL_FALSE;
	else *rval = JSVAL_TRUE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_copy(JSContext *jcx, char *filesrc, char *filedst) {
	FILE *src, *dst;
	JSBool r = JS_TRUE;
	if ((src = fopen(filesrc, "r")) != NULL) {
		if ((dst = fopen(filedst, "w")) != NULL) {
			char *b[NGX_JAVASCRIPT_BUFFER];
			size_t n;
			while ((n = fread(b, 1, NGX_JAVASCRIPT_BUFFER, src)) > 0) {
				if (n != fwrite(b, 1, n, dst)) {
					r = ngx_javascript_file_error(jcx, ngx_errno, filedst);
					break;
				}
			}
			fclose(dst);
		} else {
			r = ngx_javascript_file_error(jcx, ngx_errno, filedst);
		}
		fclose(src);
	} else {
		r = ngx_javascript_file_error(jcx, ngx_errno, filesrc);
	}
	return r;
}

JSBool ngx_javascript_file_class_copy(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	char *filesrc, *filedst;
	if (JS_ConvertArguments(jcx, argc, argv, "ss", &filesrc, &filedst) == JS_FALSE) return JS_FALSE;
	if (filesrc[0] == '/') {
		if (ngx_javascript_check_basedir(jcx, filesrc, 0) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, filesrc);
		if (filedst[0] == '/') {
			if (ngx_javascript_check_basedir(jcx, filedst, O_CREAT) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, filedst);
			if (ngx_javascript_file_copy(jcx, filesrc, filedst) == JS_FALSE) return JS_FALSE;
		} else {
			char fulldst[NGX_MAX_PATH];
			if (ngx_javascript_fullpath(jcx, filedst, fulldst) == JS_FALSE) return JS_FALSE;
			if (ngx_javascript_check_basedir(jcx, fulldst, O_CREAT) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, fulldst);
			if (ngx_javascript_file_copy(jcx, filesrc, fulldst) == JS_FALSE) return JS_FALSE;
		}
	} else {
		char fullsrc[NGX_MAX_PATH];
		if (ngx_javascript_fullpath(jcx, filesrc, fullsrc) == JS_FALSE) return JS_FALSE;
		if (ngx_javascript_check_basedir(jcx, fullsrc, 0) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, fullsrc);
		if (filedst[0] == '/') {
			if (ngx_javascript_check_basedir(jcx, filedst, O_CREAT) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, filedst);
			if (ngx_javascript_file_copy(jcx, fullsrc, filedst) == JS_FALSE) return JS_FALSE;
		} else {
			char fulldst[NGX_MAX_PATH];
			if (ngx_javascript_fullpath(jcx, filedst, fulldst) == JS_FALSE) return JS_FALSE;
			if (ngx_javascript_check_basedir(jcx, fulldst, O_CREAT) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, fulldst);
			if (ngx_javascript_file_copy(jcx, fullsrc, fulldst) == JS_FALSE) return JS_FALSE;
		}
	}
	*rval = JS_TRUE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_symlink(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	char *filesrc, *filedst;
	if (JS_ConvertArguments(jcx, argc, argv, "ss", &filesrc, &filedst) == JS_FALSE) return JS_FALSE;
	if (filedst[0] == '/') {
		if (ngx_javascript_check_basedir(jcx, filedst, O_CREAT) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, filedst);
		if (symlink(filesrc, filedst) != 0) return ngx_javascript_file_error(jcx, ngx_errno, filedst);
	} else {
		char fulldst[NGX_MAX_PATH];
		if (ngx_javascript_fullpath(jcx, filedst, fulldst) == JS_FALSE) return JS_FALSE;
		if (ngx_javascript_check_basedir(jcx, fulldst, O_CREAT) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, fulldst);
		if (symlink(filesrc, fulldst) != 0) return ngx_javascript_file_error(jcx, ngx_errno, fulldst);
	}
	*rval = JS_TRUE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_class_link(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	char *filesrc, *filedst;
	if (JS_ConvertArguments(jcx, argc, argv, "ss", &filesrc, &filedst) == JS_FALSE) return JS_FALSE;
	if (filesrc[0] == '/') {
		if (ngx_javascript_check_basedir(jcx, filesrc, 0) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, filesrc);
		if (filedst[0] == '/') {
			if (ngx_javascript_check_basedir(jcx, filedst, O_CREAT) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, filedst);
			if (link(filesrc, filedst) != 0) return ngx_javascript_file_error(jcx, ngx_errno, filedst);
		} else {
			char fulldst[NGX_MAX_PATH];
			if (ngx_javascript_fullpath(jcx, filedst, fulldst) == JS_FALSE) return JS_FALSE;
			if (ngx_javascript_check_basedir(jcx, fulldst, O_CREAT) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, fulldst);
			if (link(filesrc, fulldst) != 0) return ngx_javascript_file_error(jcx, ngx_errno, fulldst);
		}
	} else {
		char fullsrc[NGX_MAX_PATH];
		if (ngx_javascript_fullpath(jcx, filesrc, fullsrc) == JS_FALSE) return JS_FALSE;
		if (ngx_javascript_check_basedir(jcx, fullsrc, 0) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, fullsrc);
		if (filedst[0] == '/') {
			if (ngx_javascript_check_basedir(jcx, filedst, O_CREAT) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, filedst);
			if (link(fullsrc, filedst) != 0) return ngx_javascript_file_error(jcx, ngx_errno, filedst);
		} else {
			char fulldst[NGX_MAX_PATH];
			if (ngx_javascript_fullpath(jcx, filedst, fulldst) == JS_FALSE) return JS_FALSE;
			if (ngx_javascript_check_basedir(jcx, fulldst, O_CREAT) == JS_FALSE) return ngx_javascript_file_error(jcx, ENOENT, fulldst);
			if (link(fullsrc, fulldst) != 0) return ngx_javascript_file_error(jcx, ngx_errno, fulldst);
		}
	}
	*rval = JS_TRUE;
	return JS_TRUE;
}



JSBool ngx_javascript_file_object_get_bytes(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_file_t *file;
	JSString *str;
	jschar *r;
	struct stat st;
	char b[NGX_JAVASCRIPT_BUFFER];
	size_t i, j, n, t;
	
	file = JS_GetPrivate(jcx, obj);
	if (file == NULL || file->F == NULL) return ngx_javascript_file_error(jcx, 0, "File is closed");
	if (fstat(file->fd, &st) == -1) return ngx_javascript_file_error(jcx, ngx_errno, NULL);
	if (st.st_size == 0) {
		*rval = JS_GetEmptyStringValue(jcx);
		return JS_TRUE;
	}
	r = JS_malloc(jcx, (st.st_size + 1) * sizeof(jschar));
	if (r == NULL) return ngx_javascript_outofmemory(jcx);
	if (fseek(file->F, 0, SEEK_SET) == -1) {
		ngx_javascript_file_error(jcx, ngx_errno, NULL);
		goto ngx_javascript_file_object_get_bytes_error;
	}
	j = 0;
	n = st.st_size;
	while (n > 0) {
		t = n > NGX_JAVASCRIPT_BUFFER ? NGX_JAVASCRIPT_BUFFER : n;
		if (fread(b, 1, t, file->F) != t) {
			ngx_javascript_file_error(jcx, ngx_errno, NULL);
			goto ngx_javascript_file_object_get_bytes_error;
		}
		n -= t;
		for (i = 0; i < t; i ++, j ++) r[j] = (unsigned char) b[i];
	}
	str = JS_NewUCString(jcx, r, j);
	if (str == NULL) {
		ngx_javascript_outofmemory(jcx);
		goto ngx_javascript_file_object_get_bytes_error;
	}
	*rval = STRING_TO_JSVAL(str);
	
	return JS_TRUE;
	ngx_javascript_file_object_get_bytes_error:
	JS_free(jcx, r);
	return JS_FALSE;
}

JSBool ngx_javascript_file_object_get_chars(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_file_t *file;
	JSString *str;
	char *r = NULL;
	struct stat st;
	
	file = JS_GetPrivate(jcx, obj);
	if (file == NULL || file->F == NULL) return ngx_javascript_file_error(jcx, 0, "File is closed");
	if (fstat(file->fd, &st) == -1) return ngx_javascript_file_error(jcx, ngx_errno, NULL);
	if (st.st_size == 0) {
		*rval = JS_GetEmptyStringValue(jcx);
		return JS_TRUE;
	}
	r = JS_malloc(jcx, st.st_size);
	if (r == NULL) return ngx_javascript_outofmemory(jcx);
	if (fseek(file->F, 0, SEEK_SET) == -1) {
		ngx_javascript_file_error(jcx, ngx_errno, NULL);
		goto ngx_javascript_file_object_get_chars_error;
	}
	if (fread(r, 1, st.st_size, file->F) != st.st_size) {
		ngx_javascript_file_error(jcx, ngx_errno, NULL);
		goto ngx_javascript_file_object_get_chars_error;
	}
	str = JS_NewString(jcx, r, st.st_size);
	if (str == NULL) {
		ngx_javascript_outofmemory(jcx);
		goto ngx_javascript_file_object_get_chars_error;
	}
	*rval = STRING_TO_JSVAL(str);
	
	return JS_TRUE;
	ngx_javascript_file_object_get_chars_error:
	JS_free(jcx, r);
	return JS_FALSE;
}

JSBool ngx_javascript_file_object_write_bytes(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_file_t *file;
	JSString *str;
	jschar *chars;
	char b[NGX_JAVASCRIPT_BUFFER];
	size_t length, i, j, n, t;
	
	file = JS_GetPrivate(jcx, obj);
	if (file == NULL || file->F == NULL) return ngx_javascript_file_error(jcx, 0, "File is closed");
	
	if (JS_ConvertArguments(jcx, argc, argv, "S", &str) == JS_FALSE) return JS_FALSE;
	chars = JS_GetStringChars(str);
	if (chars == NULL) return JS_FALSE;
	length = JS_GetStringLength(str);
	if (length == 0) {
		*rval = INT_TO_JSVAL(0);
		return JS_TRUE;
	}
	
	j = 0;
	n = length;
	while (n > 0) {
		t = n > NGX_JAVASCRIPT_BUFFER ? NGX_JAVASCRIPT_BUFFER : n;
		for (i = 0; i < t; i ++, j ++) b[i] = (unsigned char) chars[j];
		if (fwrite(b, 1, t, file->F) != t) return ngx_javascript_file_error(jcx, ngx_errno, NULL);
		n -= t;
	}
	JS_NewNumberValue(jcx, length, rval);
	
	return JS_TRUE;
}

JSBool ngx_javascript_file_object_write_chars(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_file_t *file;
	JSString *str;
	jschar *chars;
	char *bytes;
	size_t length;
	
	file = JS_GetPrivate(jcx, obj);
	if (file == NULL || file->F == NULL) return ngx_javascript_file_error(jcx, 0, "File is closed");
	
	if (JS_ConvertArguments(jcx, argc, argv, "S", &str) == JS_FALSE) return JS_FALSE;
	chars = JS_GetStringChars(str);
	if (chars == NULL) return JS_FALSE;
	bytes = JS_GetStringBytes(str);
	if (bytes == NULL) return JS_FALSE;
	length = JS_GetStringLength(str);
	if (length == 0) {
		*rval = INT_TO_JSVAL(0);
		return JS_TRUE;
	}
	
	if (JS_EncodeCharacters(jcx, chars, length, NULL, &length) == JS_FALSE) return JS_FALSE;
	if (fwrite(bytes, 1, length, file->F) != length) return ngx_javascript_file_error(jcx, ngx_errno, NULL);
	JS_NewNumberValue(jcx, length, rval);
	
	return JS_TRUE;
}

JSBool ngx_javascript_file_object_stat(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	struct stat st;
	if (ngx_javascript_file_stat_file(jcx, obj, &st) == JS_FALSE) return JS_FALSE;
	*rval = ngx_javascript_stat_value(jcx, &st);
	return JS_TRUE;
}

JSBool ngx_javascript_file_object_eof(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_file_t *file = JS_GetPrivate(jcx, obj);
	if (file == NULL || file->F == NULL) return ngx_javascript_file_error(jcx, 0, "File is closed");
	*rval = feof(file->F) != 0 ? JSVAL_TRUE : JSVAL_FALSE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_object_lock(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	int mode;
	ngx_javascript_file_t *file = JS_GetPrivate(jcx, obj);
	if (file == NULL || file->F == NULL) return ngx_javascript_file_error(jcx, 0, "File is closed");
	if (JS_ConvertArguments(jcx, argc, argv, "i", &mode) == JS_FALSE) return JS_FALSE;
	if (flock(file->fd, mode) != 0) return ngx_javascript_file_error(jcx, ngx_errno, NULL);
	*rval = JSVAL_TRUE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_object_seek(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	jsdouble offset;
	int whence = 0;
	ngx_javascript_file_t *file = JS_GetPrivate(jcx, obj);
	if (file == NULL || file->F == NULL) return ngx_javascript_file_error(jcx, 0, "File is closed");
	if (JS_ConvertArguments(jcx, argc, argv, "di", &offset, &whence) == JS_FALSE) return JS_FALSE;
	if (fseek(file->F, offset, whence) != 0) return ngx_javascript_file_error(jcx, ngx_errno, NULL);
	*rval = JSVAL_TRUE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_object_tell(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	jsdouble r;
	ngx_javascript_file_t *file = JS_GetPrivate(jcx, obj);
	if (file == NULL || file->F == NULL) return ngx_javascript_file_error(jcx, 0, "File is closed");
	r = ftell(file->F);
	if (r < 0) return ngx_javascript_file_error(jcx, ngx_errno, NULL);
	if (JS_NewNumberValue(jcx, r, rval) == JS_FALSE) return JS_FALSE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_object_flush(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_file_t *file = JS_GetPrivate(jcx, obj);
	if (file == NULL || file->F == NULL) return ngx_javascript_file_error(jcx, 0, "File is closed");
	if (fflush(file->F) != 0) return ngx_javascript_file_error(jcx, ngx_errno, NULL);
	*rval = JSVAL_TRUE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_object_rewind(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_file_t *file = JS_GetPrivate(jcx, obj);
	if (file == NULL || file->F == NULL) return ngx_javascript_file_error(jcx, 0, "File is closed");
	rewind(file->F);
	*rval = JSVAL_TRUE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_object_truncate(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	jsdouble length;
	ngx_javascript_file_t *file = JS_GetPrivate(jcx, obj);
	if (file == NULL || file->F == NULL) return ngx_javascript_file_error(jcx, 0, "File is closed");
	if (JS_ConvertArguments(jcx, argc, argv, "d", &length) == JS_FALSE) return JS_FALSE;
	if (ftruncate(file->fd, length) != 0) return ngx_javascript_file_error(jcx, ngx_errno, NULL);
	*rval = JSVAL_TRUE;
	return JS_TRUE;
}

JSBool ngx_javascript_file_object_realpath(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	jsval result;
	char *filename;
	char realname[NGX_MAX_PATH];
	if (JS_LookupProperty(jcx, obj, "name", &result) == JS_FALSE || !JSVAL_IS_STRING(result)) return ngx_javascript_file_error(jcx, 0, "File is closed");
	filename = JS_GetStringBytes(JSVAL_TO_STRING(result));
	if (filename == NULL) return ngx_javascript_file_error(jcx, 0, "File is closed");
	if (realpath(filename, realname) != NULL) {
		JSString *str = JS_NewStringCopyZ(jcx, realname);
		if (str == NULL) return ngx_javascript_outofmemory(jcx);
		*rval = STRING_TO_JSVAL(str);
	} else {
		*rval = JSVAL_NULL;
	}
	return JS_TRUE;
}

JSBool ngx_javascript_file_object_basename(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	jsval result;
	char *filename;
	int i, l, length;
	if (JS_LookupProperty(jcx, obj, "name", &result) == JS_FALSE || !JSVAL_IS_STRING(result)) return ngx_javascript_file_error(jcx, 0, "File is closed");
	filename = JS_GetStringBytes(JSVAL_TO_STRING(result));
	if (filename == NULL) return ngx_javascript_file_error(jcx, 0, "File is closed");
	length = strlen(filename);
	i = length - 1;
	while (i >= 0 && filename[i] == '/') i --;
	l = i;
	while (i >= 0 && filename[i] != '/') i --;
	if (length <= 0 || i == l) {
		*rval = JS_GetEmptyStringValue(jcx);
		return JS_TRUE;
	} else {
		JSString *str = JS_NewStringCopyN(jcx, filename + i + 1, l - i);
		if (str == NULL) return ngx_javascript_outofmemory(jcx);
		*rval = STRING_TO_JSVAL(str);
	}
	return JS_TRUE;
}

JSBool ngx_javascript_file_object_dirname(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	jsval result;
	char *filename;
	int i, length;
	if (JS_LookupProperty(jcx, obj, "name", &result) == JS_FALSE || !JSVAL_IS_STRING(result)) return ngx_javascript_file_error(jcx, 0, "File is closed");
	filename = JS_GetStringBytes(JSVAL_TO_STRING(result));
	if (filename == NULL) return ngx_javascript_file_error(jcx, 0, "File is closed");
	length = strlen(filename);
	i = length - 1;
	while (i >= 0 && filename[i] == '/') i --;
	while (i >= 0 && filename[i] != '/') i --;
	if (length <= 0 || i <= 0) {
		JSString *str = JS_NewStringCopyZ(jcx, ".");
		if (str == NULL) return ngx_javascript_outofmemory(jcx);
		*rval = STRING_TO_JSVAL(str);
	} else {
		JSString *str = JS_NewStringCopyN(jcx, filename, i + 1);
		if (str == NULL) return ngx_javascript_outofmemory(jcx);
		*rval = STRING_TO_JSVAL(str);
	}
	return JS_TRUE;
}

JSBool ngx_javascript_file_object_close(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_file_finalize(jcx, obj);
	*rval = JSVAL_TRUE;
	return JS_TRUE;
}

void ngx_javascript_file_finalize(JSContext *jcx, JSObject *obj) {
	ngx_javascript_file_t *file = JS_GetPrivate(jcx, obj);
	if (file != NULL) {
		ngx_javascript_private_t *private = JS_GetContextPrivate(jcx);
		if (file->F != NULL) {
			fclose(file->F);
			file->F = NULL;
			file->fd = -1;
		}
		if (private != NULL && private->r != NULL) {
			ngx_pfree(private->r->pool, file);
			JS_SetPrivate(jcx, obj, NULL);
		}
	}
}

JSBool ngx_javascript_file_error(JSContext *jcx, int code, const char *message) {
	if (code != 0) {
		char e[256];
		ngx_strerror_r(code, (u_char *) e, 256);
		if (message == NULL) {
			JS_ReportError(jcx, "%s (%d)", e, code);
		} else {
			JS_ReportError(jcx, "%s: %s (%d)", message, e, code);
		}
	} else {
		JS_ReportError(jcx, "%s", message);
	}
	return JS_FALSE;
}
